package com.genius.Genius.Polymer.Array;

import com.genius.Genius.Element.Byte.ByteShake;
import com.genius.Genius.Element.Byte.ByteUtil;
import com.genius.Genius.Util.Unsafe.JavaUnSafe;
import sun.misc.Unsafe;


import java.nio.ByteBuffer;
import java.util.*;


/**
 * TODO LIST:
 * TODO Insert Delete Update method need to complete
 * TODO push function optimizer (ByteBuffer)
 * TODO Faster Indexing
 * TODO tail function complete
 * TODO Improve the type part of contactBytes function
 * TODO Support more alternative ways that ziplist can choose more serialize method.(Json ...)
 */
public class ZipList extends AbstractList implements ByteShake {

    private int size;
    private int bytesSize;
    private int tail;
    private byte[] elements;

    private final Unsafe unsafe = JavaUnSafe.getUnsafe();

    private final short DEFAULT_INIT_SIZE = 16;


    /**
     * | 00 00 00 00 | 00 | 00 00 00 00 | .... |
     *    perLength   type    size        data
     */
    private final byte ENTRY_PRE_LENGTH_OFFSET = 0;
    private final byte ENTRY_TYPE_OFFSET = ENTRY_PRE_LENGTH_OFFSET + 4;
    private final byte ENTRY_SIZE_OFFSET = ENTRY_TYPE_OFFSET+1;
    private final byte ENTRY_ELEMENT_OFFSET = ENTRY_SIZE_OFFSET+4;


    public ZipList(int capacity){
        elements = new byte[capacity];
        size = bytesSize = 0;
        tail = 0;
    }

    public ZipList(){
        elements = new byte[DEFAULT_INIT_SIZE];
        size = bytesSize = 0;
        tail = 0;
    }

    public boolean push(Object obj){
        ByteBuffer buffer = contactBytes(obj);
        if(Optional.ofNullable(buffer).isEmpty()){
            return false;
        }
        byte[] array = buffer.array();
        if (!ensureCapacity(array)) {
            return false;
        }
        int tempByteSize = bytesSize;
        tail = bytesSize;
        bytesSize+=array.length;
        size++;
        System.arraycopy(array,0,elements,tempByteSize,array.length);
        return true;
    }

    private boolean ensureCapacity(byte[] array){
        int oldCapacity = elements.length;
        if(array.length>Integer.MAX_VALUE-bytesSize){
            return false;
        }
        int newCapacity = array.length+bytesSize;
        if(newCapacity>=oldCapacity){
            grow(oldCapacity,newCapacity);
        }
        return true;
    }

    private int newCapacity(int newCapacity){
        int generate = 1024;
        if(newCapacity>Integer.MAX_VALUE/2){
            newCapacity = newCapacity>Integer.MAX_VALUE-generate?Integer.MAX_VALUE:newCapacity+generate;
        }else{
            newCapacity = newCapacity<<1;
        }
        return newCapacity;
    }

    private void grow(int oldCapacity,int newCapacity){
        if(newCapacity==oldCapacity&&newCapacity==Integer.MAX_VALUE){
           return;
        }
        int newSize = newCapacity(newCapacity);
        elements = Arrays.copyOf(elements,newSize);
    }



    private ByteBuffer contactBytes(Object obj){
        byte[] objBytes = convertElement(obj);
        int objSize;
        if((objSize=objBytes.length)<=0){
            return null;//TODO throw Exception
        }
        int preLength =bytesSize - tail;
        byte[] preLengthBytes = ByteUtil.int2Byte(preLength);
        byte[] sizeBytes = ByteUtil.int2Byte(objSize);
        byte[] type = {1}; //TODO type
        ByteBuffer buf = ByteBuffer.allocate(ENTRY_ELEMENT_OFFSET+objSize);
        buf.put(preLengthBytes);
        buf.put(type);
        buf.put(sizeBytes);
        buf.put(objBytes);
        buf.flip();
        return buf;
    }

    private byte[] convertElement(Object obj){
        return ByteUtil.quickParseBytes(obj);
    }

    @Override
    public Object get(int index) {
        if(index<0||index>=size){
            return null;//TODO outofindex
        }
        byte[] bytes = getBytes(index);
        return ByteUtil.quickParseObject(bytes,String.class);
    }

    private byte[] getBytes(int index){
        int current = 0;
        int point = 0;
        byte[] data = {};
        while (current<index){
            point = nextNode(point);
            if(point==-1){
                return data;
            }
            current++;
        }
        data = content(point);
        return data;
    }

    public Object top(){
        return get(0);
    }

    public byte[] tail(){
        return null;
    }

    @Override
    public int size() {
        return size;
    }

    private boolean rangeCheck(int index,int range){
        if(elements.length<range||elements.length-index<range){
            return false;
        }
        return true;
    }

    private int entryCatch(int start,int end){
        return  ByteUtil.parseInt(Arrays.copyOfRange(elements,start,end));
    }

    private byte type(int index){
       if(!rangeCheck(index,ENTRY_TYPE_OFFSET)){
           return -1;
       }
       return elements[index+ENTRY_TYPE_OFFSET];
    }

    private byte[] content(int index){
        int start = index+ENTRY_ELEMENT_OFFSET;
        int size = entryElementSize(index);
        int end = start + size;
        if(end>elements.length){
            return null;
        }
        return Arrays.copyOfRange(elements,start,end);
    }

    private int entryElementSize(int index){
        if(!rangeCheck(index,ENTRY_ELEMENT_OFFSET)){
            return -1;
        }
        int start = index+ENTRY_SIZE_OFFSET;
        int end = index+ENTRY_ELEMENT_OFFSET;
        return entryCatch(start,end);
    }

    private int preElementSize(int index){
        if(!rangeCheck(index,ENTRY_TYPE_OFFSET)){
            return -1;
        }
        int start = index+ENTRY_PRE_LENGTH_OFFSET;
        int end = start+ENTRY_TYPE_OFFSET;
        return entryCatch(start,end);
    }

    private int preNode(int index){
        if(index==0){
            return 0;
        }
        int pre_size = preElementSize(index);
        int pre_index = index-pre_size;
        return pre_index<0?-1:pre_index;
    }

    private int nextNode(int index){
        int end = index+ENTRY_ELEMENT_OFFSET;
        int size = entryElementSize(index);
        if(size<0){
            return -1;
        }
        int next_index = end+size;
        if(next_index>=elements.length){
            return -1;
        }
        return next_index;
    }
}
